package com.cqcomponents.managers;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.api.resource.ValueMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.cqcomponents.annotations.ComponentInjection;
import com.cqcomponents.annotations.JcrPropertyInjection;
import com.cqcomponents.api.cache.IComponentCache;
import com.cqcomponents.api.cache.IContentCache;
import com.cqcomponents.api.components.IComponent;
import com.cqcomponents.api.components.IComponentManager;
import com.cqcomponents.api.exceptions.MandatoryComponentException;
import com.cqcomponents.cache.NoopComponentCache;
import com.cqcomponents.cache.NoopContentCache;
import com.cqcomponents.components.BaseComponent;
import com.cqcomponents.components.Contentreference;
import com.cqcomponents.utils.ComponentUtils;
import com.day.cq.wcm.api.WCMMode;

@Component(immediate = true)
@Service
public class ComponentManager implements IComponentManager {
	protected Logger logger = LoggerFactory.getLogger(this.getClass());

	@Reference(cardinality = ReferenceCardinality.OPTIONAL_UNARY, policy = ReferencePolicy.DYNAMIC)
	private IComponentCache componentCache;

	@Reference(cardinality = ReferenceCardinality.OPTIONAL_UNARY, policy = ReferencePolicy.DYNAMIC)
	private IContentCache contentCache;

	@Override
	public IComponent getComponentFromResource(Resource resource)
			throws MandatoryComponentException {
		IComponent result = null;

		Class<IComponent> clazz = null;
		try {
			clazz = ComponentUtils.resourceToComponentClass(resource);
			String currentPagePath = ComponentUtils.getPageResource(resource)
					.getPath();
			result = this.getComponentFromResourceOrClass(resource, clazz,
					currentPagePath);

		} catch (MandatoryComponentException cnae) {
			throw cnae;
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			throw new MandatoryComponentException(resource.getPath(), clazz, e);
		}

		return result;
	}

	private IComponent getComponentFromCacheByResourcePath(String resourcePath) {
		return getComponentCache().getComponent(resourcePath);
	}

	private IComponentCache getComponentCache() {
		if (this.componentCache == null) {
			this.logger
					.info("No component cache found. Running without caching support "
							+ "for components using the NoopComponentBeanCache");
			return NoopComponentCache.getInstance();
		} else {
			return this.componentCache;
		}
	}

	private IContentCache getContentCache() {
		if (this.contentCache == null) {
			this.logger
					.info("No content cache found. Running without caching support "
							+ "for content using the NoopContentCache");
			return NoopContentCache.getInstance();
		} else {
			return this.contentCache;
		}
	}

	private IComponent getComponentFromResourceOrClass(Resource resource,
			Class<IComponent> clazz, String currentPagePath)
			throws ClassNotFoundException, InstantiationException,
			IllegalAccessException, IllegalArgumentException,
			InvocationTargetException, MandatoryComponentException {

		IComponent result = getComponentFromCacheByResourcePath(resource
				.getPath());
		if (result == null) {
			// Component is not in the cache yet -- Initialize new one
			result = clazz.newInstance();

			injectPropertiesAndComponents(resource, result, currentPagePath);

			if (result instanceof Contentreference) {
				// Inject referencedResourceType
				Contentreference contentReference = ((Contentreference) result);
				String referencedPath = contentReference.getReferencedPath();
				Resource referencedResource = resource.getResourceResolver()
						.getResource(referencedPath);
				if (referencedResource != null) {
					String resourceType = referencedResource.getResourceType();
					contentReference.setReferencedResourceType(resourceType);
				}
			}

			// Set pagePath and resourcePath
			String pagePath = ComponentUtils.getPageResource(resource)
					.getPath();
			((BaseComponent) result).setPagePath(pagePath);
			((BaseComponent) result).setResourcePath(resource.getPath());

			// Add component to cache if caching is not disabled
			if (!ComponentUtils.isCachingDisabled(resource, result)) {
				getComponentCache().addToCache(resource.getPath(), result);
			}
		}

		// Set currentpagePath
		((BaseComponent) result).setCurrentPagePath(currentPagePath);

		return result;
	}

	@SuppressWarnings("unchecked")
	private void injectPropertiesAndComponents(Resource resource,
			IComponent component, String currentPagePath)
			throws IllegalArgumentException, IllegalAccessException,
			InvocationTargetException, ClassNotFoundException,
			InstantiationException, MandatoryComponentException {
		Method[] methods = component.getClass().getMethods();

		for (Method method : methods) {
			String methodNameWithoutSet = ComponentUtils
					.getMethodNameWithoutSet(method);
			if (method.isAnnotationPresent(ComponentInjection.class)) {
				ComponentInjection annotation = method
						.getAnnotation(ComponentInjection.class);

				// Child name to look for component data
				String childName = annotation.childName();
				boolean mandatory = annotation.mandatory();

				Class<IComponent> clazz = (Class<IComponent>) method
						.getParameterTypes()[0];
				if (Collection.class.isAssignableFrom(clazz)) {
					// Inject list of components
					// Path of the root resource where to look for children
					String rootPathAttribute = annotation.rootPathAttribute();
					Resource root = null;
					if (StringUtils.isNotBlank(rootPathAttribute)) {
						String rootPath = ResourceUtil.getValueMap(resource)
								.get(rootPathAttribute, String.class);
						root = resource.getResourceResolver().getResource(
								rootPath);
					}

					Type[] types = method.getGenericParameterTypes();
					ParameterizedType pType = (ParameterizedType) types[0];
					clazz = (Class<IComponent>) pType.getActualTypeArguments()[0];

					String primaryType = annotation.primaryType();

					if (root == null) {
						// Not root --> Set it to current resource
						root = resource;
					}
					List<IComponent> components = collectSubResources(root,
							clazz, childName, primaryType, currentPagePath);

					if (components.size() == 0 && mandatory) {
						// Component is mandatory but not available --> throw
						// exception
						throw new MandatoryComponentException(
								resource.getPath(),
								(Class<IComponent>) component.getClass(),
								clazz, childName);
					} else if (components.size() > 0) {
						method.invoke(component, components);
					}
				} else {
					Resource subResource = resource.getResourceResolver()
							.getResource(resource.getPath() + "/" + childName);
					if (subResource == null) {
						throw new MandatoryComponentException(
								resource.getPath(),
								(Class<IComponent>) component.getClass(),
								clazz, childName);
					} else {
						// Sub resource exists. In this case we use the class
						// from
						// the first parameter from the setter method
						IComponent subComponent = getComponentFromResourceOrClass(
								subResource, clazz, currentPagePath);
						method.invoke(component, subComponent);
					}
				}

			} else if (method.isAnnotationPresent(JcrPropertyInjection.class)) {
				JcrPropertyInjection annotation = method
						.getAnnotation(JcrPropertyInjection.class);
				String name = StringUtils.defaultIfEmpty(annotation.name(),
						methodNameWithoutSet);

				// Now read property with that name
				Object value = null;

				try {
					Class<?> parameterType = method.getParameterTypes()[0];
					if ("cq:Page".equals(resource.getResourceType())) {
						value = resource.getChild("jcr:content")
								.adaptTo(ValueMap.class).get(name);

					} else {
						value = resource.adaptTo(ValueMap.class).get(name);
					}
					if (value != null) {
						// Handle primitive types
						if (parameterType == int.class) {
							method.invoke(component,
									Integer.parseInt(value.toString()));
						} else if (parameterType == long.class) {
							method.invoke(component,
									Long.parseLong(value.toString()));
						} else if (parameterType == byte.class) {
							method.invoke(component,
									Byte.parseByte(value.toString()));
						} else if (parameterType == double.class) {
							method.invoke(component,
									Double.parseDouble(value.toString()));
						} else if (parameterType == float.class) {
							method.invoke(component,
									Float.parseFloat(value.toString()));
						} else if (parameterType == boolean.class) {
							method.invoke(component,
									Boolean.parseBoolean(value.toString()));
						} else if (parameterType == char.class) {
							method.invoke(component, value.toString().charAt(0));
						} else {
							method.invoke(component, value);
						}
					}
				} catch (Exception e) {
					logger.debug("No value found for property: " + name
							+ " under resource: " + resource.getPath());
				}
			}

		}

	}

	private List<IComponent> collectSubResources(Resource resource,
			Class<IComponent> clazz, String childName, String primaryType,
			String currentPagePath) throws IllegalArgumentException,
			ClassNotFoundException, InstantiationException,
			IllegalAccessException, InvocationTargetException,
			MandatoryComponentException {
		List<IComponent> components = new ArrayList<IComponent>();
		// Look beneath resource for resources starting with
		// childName
		Iterator<Resource> resourceIterator = resource.getResourceResolver()
				.listChildren(resource);
		while (resourceIterator.hasNext()) {
			Resource subResource = resourceIterator.next();
			String resourcePrimaryType = ResourceUtil.getValueMap(subResource)
					.get("jcr:primaryType", String.class);
			if ((StringUtils.isBlank(childName) || subResource.getPath()
					.contains(childName))
					&& (StringUtils.isBlank(primaryType) || primaryType
							.equals(resourcePrimaryType))) {
				IComponent subComponent = getComponentFromResourceOrClass(
						subResource, clazz, currentPagePath);
				components.add(subComponent);
			}
		}

		return components;
	}

	@Override
	public String getCachedContent(WCMMode wcmMode, String path) {
		return this.getContentCache().getContent(wcmMode, path);
	}

	@Override
	public void addToCache(WCMMode wcmMode, String path, String content) {
		this.getContentCache().addToCache(wcmMode, path, content);

	}

	@Override
	public void removeFromCache(Set<String> pathsToRemove) {
		// Remove from content cache
		this.getContentCache().removeFromCache(pathsToRemove);
		// Remove from component cache
		this.getComponentCache().removeFromCache(pathsToRemove);

	}

}
